#!/bin/sh

###################################################################################
# 此脚本用于检测 git 状态
# Copy from https://github.com/fboender/multi-git-status
###################################################################################

# MIT license

if [ -t 1 ]; then
	# Our output is not being redirected, so we can use colors.
	C_RED="\033[1;31m"
	C_GREEN="\033[1;32m"
	C_YELLOW="\033[1;33m"
	C_BLUE="\033[1;34m"
	C_PURPLE="\033[1;35m"
	C_CYAN="\033[1;36m"
	C_RESET="$(tput sgr0)"
fi

C_OK="$C_GREEN"
C_LOCKED="$C_RED"
C_NEEDS_PUSH="$C_YELLOW"
C_NEEDS_PULL="$C_BLUE"
C_NEEDS_COMMIT="$C_RED"
C_NEEDS_UPSTREAM="$C_PURPLE"
C_UNTRACKED="$C_CYAN"
C_STASHES="$C_YELLOW"

DEBUG=0

usage() {
	cat << EOF >&2

Usage: $0 [-w] [-e] [-f] [--no-X] [DIR] [DEPTH=2]

Scan for .git dirs under DIR (up to DEPTH dirs deep) and show git status

  -w   Warn about dirs that are not Git repositories
  -e   Exclude repos that are 'ok'
  -f   Do a 'git fetch' on each repo (slow for many repos)

You can limit output with the following options:

  --no-push
  --no-pull
  --no-upstream
  --no-uncommitted
  --no-untracked
  --no-stashes

EOF
}

# Handle commandline options
WARN_NOT_REPO=0
EXCLUDE_OK=0
DO_FETCH=0
NO_PUSH=0
NO_PULL=0
NO_UPSTREAM=0
NO_UNCOMMITTED=0
NO_UNTRACKED=0
NO_STASHES=0

while [ \! -z "$1" ]; do
	# Stop reading when we've run out of options.
	[ "$(echo "$1" | cut -c 1)" != "-" ] && break

	if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
		usage
		exit 1
	fi
	if [ "$1" = "-w" ]; then
		WARN_NOT_REPO=1
	fi
	if [ "$1" = "-e" ]; then
		EXCLUDE_OK=1
	fi
	if [ "$1" = "-f" ]; then
		DO_FETCH=1
	fi
	if [ "$1" = "--no-push" ]; then
		NO_PUSH=1
	fi
	if [ "$1" = "--no-pull" ]; then
		NO_PULL=1
	fi
	if [ "$1" = "--no-upstream" ]; then
		NO_UPSTREAM=1
	fi
	if [ "$1" = "--no-uncommitted" ]; then
		NO_UNCOMMITTED=1
	fi
	if [ "$1" = "--no-untracked" ]; then
		NO_UNTRACKED=1
	fi
	if [ "$1" = "--no-stashes" ]; then
		NO_STASHES=1
	fi

	shift
done

if [ -z "$1" ]; then
	ROOT_DIR="."
else
	ROOT_DIR="$1"
fi

if [ -z "$2" ]; then
	DEPTH=2
else
	DEPTH="$2"
fi

# Find all .git dirs, up to DEPTH levels deep
find -L "$ROOT_DIR" -maxdepth "$DEPTH" -type d | while read -r PROJ_DIR
do
	GIT_DIR="$PROJ_DIR/.git"

	# If this dir is not a repo, and WARN_NOT_REPO is 1, tell the user.
	if [ \! -d "$GIT_DIR" ]; then
		if [ "$WARN_NOT_REPO" -eq 1 ] && [ "$PROJ_DIR" != "." ]; then
			printf "${PROJ_DIR}: not a git repo\n"
		fi
		continue
	fi

	[ $DEBUG -eq 1 ] && echo "${PROJ_DIR}"

	# Check if repo is locked
	if [ -f "$GIT_DIR/index.lock" ]; then
		printf "${PROJ_DIR}: ${C_LOCKED}Locked. Skipping.${C_RESET}\n"
		continue
	fi

	# Do a 'git fetch' if requested
	if [ "$DO_FETCH" -eq 1 ]; then
		git --work-tree "$(dirname "$GIT_DIR")" --git-dir "$GIT_DIR" fetch -q > /dev/null
	fi

	# Refresh the index, or we might get wrong results.
	git --work-tree "$(dirname "$GIT_DIR")" --git-dir "$GIT_DIR" update-index -q --refresh > /dev/null 2>&1

	# Find all remote branches that have been checked out and figure out if
	# they need a push or pull. We do this with various tests and put the name
	# of the branches in NEEDS_XXXX, seperated by newlines. After we're done,
	# we remove duplicates from NEEDS_XXX.
	NEEDS_PUSH_BRANCHES=""
	NEEDS_PULL_BRANCHES=""
	NEEDS_UPSTREAM_BRANCHES=""

	for REF_HEAD in $(cd "$GIT_DIR/refs/heads" && find . -type 'f' | sed "s/^\.\///"); do
		# Check if this branch is tracking an upstream (local/remote branch)
		UPSTREAM=$(git --git-dir "$GIT_DIR" rev-parse --abbrev-ref --symbolic-full-name "$REF_HEAD@{u}" 2> /dev/null)
		EXIT_CODE="$?"
		if [ "$EXIT_CODE" -eq 0 ]; then
			# Branch is tracking a remote branch. Find out how much behind /
			# ahead it is of that remote branch.
			CNT_AHEAD_BEHIND=$(git --git-dir "$GIT_DIR" rev-list --left-right --count "$REF_HEAD...$UPSTREAM")
			CNT_AHEAD=$(echo "$CNT_AHEAD_BEHIND" | awk '{ print $1 }')
			CNT_BEHIND=$(echo "$CNT_AHEAD_BEHIND" | awk '{ print $2 }')

			[ $DEBUG -eq 1 ] && echo "CNT_AHEAD_BEHIND: $CNT_AHEAD_BEHIND"
			[ $DEBUG -eq 1 ] && echo "CNT_AHEAD: $CNT_AHEAD"
			[ $DEBUG -eq 1 ] && echo "CNT_BEHIND: $CNT_BEHIND"

			if [ "$CNT_AHEAD" -gt 0 ]; then
				NEEDS_PUSH_BRANCHES="${NEEDS_PUSH_BRANCHES}\n$REF_HEAD"
			fi
			if [ "$CNT_BEHIND" -gt 0 ]; then
				NEEDS_PULL_BRANCHES="${NEEDS_PULL_BRANCHES}\n$REF_HEAD"
			fi

			# Check if this branch is a branch off another branch. and if it needs
			# to be updated.
			REV_LOCAL=$(git --git-dir "$GIT_DIR" rev-parse --verify "$REF_HEAD" 2> /dev/null)
			REV_REMOTE=$(git --git-dir "$GIT_DIR" rev-parse --verify "$UPSTREAM" 2> /dev/null)
			REV_BASE=$(git --git-dir "$GIT_DIR" merge-base "$REF_HEAD" "$UPSTREAM" 2> /dev/null)

			[ $DEBUG -eq 1 ] && echo "REV_LOCAL: $REV_LOCAL"
			[ $DEBUG -eq 1 ] && echo "REV_REMOTE: $REV_REMOTE"
			[ $DEBUG -eq 1 ] && echo "REV_BASE: $REV_BASE"

			if [ "$REV_LOCAL" = "$REV_REMOTE" ]; then
				: # NOOP
			else
				if [ "$REV_LOCAL" = "$REV_BASE" ]; then
					NEEDS_PULL_BRANCHES="${NEEDS_PULL_BRANCHES}\n$REF_HEAD"
				fi
				if [ "$REV_REMOTE" = "$REV_BASE" ]; then
					NEEDS_PUSH_BRANCHES="${NEEDS_PUSH_BRANCHES}\n$REF_HEAD"
				fi
			fi
		else
			# Branch does not have an upstream (local/remote branch).
			NEEDS_UPSTREAM_BRANCHES="${NEEDS_UPSTREAM_BRANCHES}\n$REF_HEAD"
		fi

	done

	# Remove duplicates from NEEDS_XXXX and make comma-seperated
	NEEDS_PUSH_BRANCHES=$(printf "$NEEDS_PUSH_BRANCHES" | sort | uniq | tr '\n' ',' | sed "s/^,\(.*\),$/\1/")
	NEEDS_PULL_BRANCHES=$(printf "$NEEDS_PULL_BRANCHES" | sort | uniq | tr '\n' ',' | sed "s/^,\(.*\),$/\1/")
	NEEDS_UPSTREAM_BRANCHES=$(printf "$NEEDS_UPSTREAM_BRANCHES" | sort | uniq | tr '\n' ',' | sed "s/^,\(.*\),$/\1/")

	# Find out if there are unstaged, uncommitted or untracked changes
	UNSTAGED=$(git --work-tree "$(dirname "$GIT_DIR")" --git-dir "$GIT_DIR" diff-index --quiet HEAD -- 2> /dev/null;
	echo $?)
	UNCOMMITTED=$(git --work-tree "$(dirname "$GIT_DIR")" --git-dir "$GIT_DIR" diff-files --quiet --ignore-submodules --;
	echo $?)
	UNTRACKED=$(git --work-tree "$(dirname "$GIT_DIR")" --git-dir "$GIT_DIR" ls-files --exclude-standard --others)
	cd "$(dirname "$GIT_DIR")" || exit
	STASHES=$(git stash list | wc -l)
	cd "$OLDPWD" || exit

	# Build up the status string
	IS_OK=0 # 0 = Repo needs something, 1 = Repo needs nothing ('ok')
	STATUS_NEEDS=""
	if [ \! -z "$NEEDS_PUSH_BRANCHES" ] && [ "$NO_PUSH" -eq 0 ]; then
		STATUS_NEEDS="${STATUS_NEEDS}${C_NEEDS_PUSH}Needs push ($NEEDS_PUSH_BRANCHES)${C_RESET} "
	fi
	if [ \! -z "$NEEDS_PULL_BRANCHES" ] && [ "$NO_PULL" -eq 0 ]; then
		STATUS_NEEDS="${STATUS_NEEDS}${C_NEEDS_PULL}Needs pull ($NEEDS_PULL_BRANCHES)${C_RESET} "
	fi
	if [ \! -z "$NEEDS_UPSTREAM_BRANCHES" ] && [ "$NO_UPSTREAM" -eq 0 ]; then
		STATUS_NEEDS="${STATUS_NEEDS}${C_NEEDS_UPSTREAM}Needs upstream ($NEEDS_UPSTREAM_BRANCHES)${C_RESET} "
	fi
	if [ "$UNSTAGED" -ne 0 ] || [ "$UNCOMMITTED" -ne 0 ] && [ "$NO_UNCOMMITTED" -eq 0 ]; then
		STATUS_NEEDS="${STATUS_NEEDS}${C_NEEDS_COMMIT}Uncommitted changes${C_RESET} "
	fi
	if [ "$UNTRACKED" != "" ] && [ "$NO_UNTRACKED" -eq 0 ]; then
		STATUS_NEEDS="${STATUS_NEEDS}${C_UNTRACKED}Untracked files${C_RESET} "
	fi
	if [ "$STASHES" -ne 0 ] && [ "$NO_STASHES" -eq 0 ]; then
		STATUS_NEEDS="${STATUS_NEEDS}${C_STASHES}$STASHES stashes${C_RESET} "
	fi
	if [ "$STATUS_NEEDS" = "" ]; then
		IS_OK=1
		STATUS_NEEDS="${STATUS_NEEDS}${C_OK}ok${C_RESET} "
	fi

	# Print the output, unless repo is 'ok' and -e was specified
	if [ "$IS_OK" -ne 1 ] || [ "$EXCLUDE_OK" -ne 1 ]; then
		printf "${PROJ_DIR}: $STATUS_NEEDS\n"
	fi
done
